/*-------------------------------------------------------------------------
 *
 * hba.c
 *	  处理基于主机的身份验证的例程（该方案是通过查看系统说他来自哪个IP地址并根据它选择身份验证方法来验证用户）。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/libpq/hba.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <ctype.h>
#include <pwd.h>
#include <fcntl.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "access/htup_details.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_type.h"
#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "miscadmin.h"
#include "postmaster/postmaster.h"
#include "regex/regex.h"
#include "replication/walsender.h"
#include "storage/fd.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/varlena.h"

#ifdef USE_LDAP
#ifdef WIN32
#include <winldap.h>
#else
#include <ldap.h>
#endif
#endif


#define MAX_TOKEN	10240

/* 回调数据用于 check_network_callback */
typedef struct check_network_data
{
	IPCompareMethod method;		/* 测试方法 */
	SockAddr   *raddr;			/* 客户端的实际地址 */
	bool		result;			/* 如果匹配则设为 true */
} check_network_data;


#define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
#define token_matches(t, k)  (strcmp(t->string, k) == 0)

/*
 * HBA 配置文件的预解析内容：HbaLine 结构体的列表。
 * parsed_hba_context 是它所在的内存上下文。
 */
static List *parsed_hba_lines = NIL;
static MemoryContext parsed_hba_context = NULL;

/*
 * ident 映射文件的预解析内容：IdentLine 结构体的列表。
 * parsed_ident_context 是它所在的内存上下文。
 *
 * 注意：IdentLine 结构体可以包含在内存上下文之外的预编译正则表达式。
 * 在销毁或重置内存上下文之前，需要显式释放它们。
 */
static List *parsed_ident_lines = NIL;
static MemoryContext parsed_ident_context = NULL;

/*
 * 以下字符数组表示 PostgreSQL 支持的认证方法的名称。
 *
 * 注意：保持此与 hba.h 中的 UserAuth 枚举同步。
 */
static const char *const UserAuthName[] =
{
	"reject",
	"implicit reject",			/* 不是用户可见的选项 */
	"trust",
	"ident",
	"password",
	"md5",
#ifdef FDDGM
	"sm3",
#endif
	"scram-sha-256",
	"gss",
	"sspi",
	"pam",
	"bsd",
	"ldap",
	"cert",
	"radius",
	"peer"
};


static List *fc_tokenize_inc_file(List *fc_tokens, const char *fc_outer_filename,
							   const char *fc_inc_filename, int fc_elevel, char **fc_err_msg);
static bool fc_parse_hba_auth_opt(char *fc_name, char *fc_val, HbaLine *fc_hbaline,
							   int fc_elevel, char **fc_err_msg);


/*
 * isblank() 存在于 ISO C99 规范中，但目前还不太便携，
 * 所以提供我们自己的版本。
 */
bool pg_isblank(const char fc_c)
{
	return fc_c == ' ' || fc_c == '\t' || fc_c == '\r';
}


/*
 * 从 *lineptr 所指向的字符串中抓取一个标记。
 *
 * 标记是由非空白字符组成的字符串，边界由空白字符、
 * 逗号、行的开始和行的结束界定。空白是指空格或制表符。
 *
 * 标记可以由双引号分隔（这允许包含空白或 '#'，但不允许换行）。
 * 与 SQL 类似，写两个双引号来表示一个双引号。
 *
 * 以未引用的 '#' 开始的注释会被跳过，即行的其余部分会被忽略。
 *
 * （注意，行连续处理在标记化之前进行。
 * 因此，如果在引用文本或注释中发生连续，引用文本或注释将被认为延续到下一行。）
 *
 * 如果有标记，返回该标记到 *buf（大小为 bufsz 的缓冲区），并将
 * *lineptr 移动到标记之后。
 *
 * 此外，我们设置 *initial_quote 以指示在第一个字符之前是否有引号。
 * （我们用它来防止 "@x" 被视为文件包含请求。注意 @"x" 应该被如此处理；
 * 我们想允许这支持文件路径中的嵌入空格。）
 *
 * 我们设置 *terminating_comma 以指示标记是否以逗号结束（该逗号不返回）。
 *
 * 如果发生错误，以 ereport 等级 elevel 记录一条消息，并且
 * 同时将 *err_msg 设置为描述错误的字符串。目前唯一可能的错误是标记对 buf 太长。
 *
 * 如果成功：将空终止的标记存储在 *buf 中并返回 true。
 * 如果行上没有更多标记：设置 *buf = '\0' 并返回 false。
 * 如果发生错误：填充 buf 为截断或格式错误的标记并返回 false。
 */
static bool fc_next_token(char **fc_lineptr, char *fc_buf, int fc_bufsz,
		   bool *fc_initial_quote, bool *fc_terminating_comma,
		   int fc_elevel, char **fc_err_msg)
{
	int			fc_c;
	char	   *fc_start_buf = fc_buf;
	char	   *fc_end_buf = fc_buf + (fc_bufsz - 1);
	bool		fc_in_quote = false;
	bool		fc_was_quote = false;
	bool		fc_saw_quote = false;

	Assert(fc_end_buf > fc_start_buf);

	*fc_initial_quote = false;
	*fc_terminating_comma = false;

	/* 跳过下一个标记前方的任何空白和逗号 */
	while ((fc_c = (*(*fc_lineptr)++)) != '\0' && (pg_isblank(fc_c) || fc_c == ','))
		;

	/*
	 * 在 buf 中构建恰好到 EOL、未引用的逗号或
	 * 未引用的空白字符的下一个字符的标记。
	 */
	while (fc_c != '\0' &&
		   (!pg_isblank(fc_c) || fc_in_quote))
	{
		/* 跳过到 EOL 的注释 */
		if (fc_c == '#' && !fc_in_quote)
		{
			while ((fc_c = (*(*fc_lineptr)++)) != '\0')
				;
			break;
		}

		if (fc_buf >= fc_end_buf)
		{
			*fc_buf = '\0';
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("authentication file token too long, skipping: \"%s\"",
							fc_start_buf)));
			*fc_err_msg = "authentication file token too long";
			/* 丢弃行的其余部分 */
			while ((fc_c = (*(*fc_lineptr)++)) != '\0')
				;
			/* 恢复 '\0'，以防我们再次被调用 */
			(*fc_lineptr)--;
			return false;
		}

		/* 我们不在令牌中返回终止逗号 */
		if (fc_c == ',' && !fc_in_quote)
		{
			*fc_terminating_comma = true;
			break;
		}

		if (fc_c != '"' || fc_was_quote)
			*fc_buf++ = fc_c;

		/* 字面双引号是两个双引号 */
		if (fc_in_quote && fc_c == '"')
			fc_was_quote = !fc_was_quote;
		else
			fc_was_quote = false;

		if (fc_c == '"')
		{
			fc_in_quote = !fc_in_quote;
			fc_saw_quote = true;
			if (fc_buf == fc_start_buf)
				*fc_initial_quote = true;
		}

		fc_c = *(*fc_lineptr)++;
	}

	/*
	 * 在令牌后面未处理的字符（在它是'\0'时是关键，
	 * 否则下一次调用将读取字符串末尾之外的内容）。
	 */
	(*fc_lineptr)--;

	*fc_buf = '\0';

	return (fc_saw_quote || fc_buf > fc_start_buf);
}

/*
 * 构造一个已分配内存的 AuthToken 结构体，复制给定的字符串。
 */
static AuthToken *
fc_make_auth_token(const char *fc_token, bool fc_quoted)
{
	AuthToken  *fc_authtoken;
	int			fc_toklen;

	fc_toklen = strlen(fc_token);
	/* 我们将字符串复制到与结构体相同的已分配内存块中 */
	fc_authtoken = (AuthToken *) palloc(sizeof(AuthToken) + fc_toklen + 1);
	fc_authtoken->string = (char *) fc_authtoken + sizeof(AuthToken);
	fc_authtoken->quoted = fc_quoted;
	memcpy(fc_authtoken->string, fc_token, fc_toklen + 1);

	return fc_authtoken;
}

/*
 * 将 AuthToken 结构体复制到新分配的内存中。
 */
static AuthToken *
fc_copy_auth_token(AuthToken *fc_in)
{
	AuthToken  *fc_out = fc_make_auth_token(fc_in->string, fc_in->quoted);

	return fc_out;
}


/*
 * 从一行中标记化一个 HBA 字段，处理文件包含和逗号列表。
 *
 * filename: 当前文件的路径名（用于解析相对路径名）
 * *lineptr: 当前行指针，将在字段之后推进
 *
 * 如果发生错误，以 ereport 级别 elevel 记录消息，并将 *err_msg 设置为描述错误的字符串。请注意，结果可能仍然非 NIL，因此必须测试 *err_msg 以确定是否发生错误。
 *
 * 结果是一个 AuthToken 结构体的列表，每个令牌对应字段中的一个，
 * 或者如果到达行尾则为 NIL。
 */
static List * fc_next_field_expand(const char *fc_filename, char **fc_lineptr,
				  int fc_elevel, char **fc_err_msg)
{
	char		fc_buf[MAX_TOKEN];
	bool		fc_trailing_comma;
	bool		fc_initial_quote;
	List	   *fc_tokens = NIL;

	do
	{
		if (!fc_next_token(fc_lineptr, fc_buf, sizeof(fc_buf),
						&fc_initial_quote, &fc_trailing_comma,
						fc_elevel, fc_err_msg))
			break;

		/* 这是引用一个文件吗？ */
		if (!fc_initial_quote && fc_buf[0] == '@' && fc_buf[1] != '\0')
			fc_tokens = fc_tokenize_inc_file(fc_tokens, fc_filename, fc_buf + 1,
									   fc_elevel, fc_err_msg);
		else
			fc_tokens = lappend(fc_tokens, fc_make_auth_token(fc_buf, fc_initial_quote));
	} while (fc_trailing_comma && (*fc_err_msg == NULL));

	return fc_tokens;
}

/*
 * tokenize_inc_file
 *		从另一个文件扩展一个文件包含到一个 hba "字段"
 *
 * 打开并标记化一个从另一个 HBA 配置文件包含的文件，带 @，
 * 并将其返回的所有值作为 AuthTokens 的扁平列表。如果发现 @-令牌，则递归展开它。新读取的令牌附加到 "tokens"（确保 foo,bar,@baz 按照预期工作）。
 * 所有新令牌都在调用者的内存上下文中分配。
 *
 * 如果发生错误，以 ereport 级别 elevel 记录消息，并将 *err_msg 设置为描述错误的字符串。请注意，结果可能仍然非 NIL，因此必须测试 *err_msg 以确定是否发生错误。
 */
static List * fc_tokenize_inc_file(List *fc_tokens,
				  const char *fc_outer_filename,
				  const char *fc_inc_filename,
				  int fc_elevel,
				  char **fc_err_msg)
{
	char	   *fc_inc_fullname;
	FILE	   *fc_inc_file;
	List	   *fc_inc_lines;
	ListCell   *fc_inc_line;
	MemoryContext fc_linecxt;

	if (is_absolute_path(fc_inc_filename))
	{
		/* 绝对路径按原样使用 */
		fc_inc_fullname = pstrdup(fc_inc_filename);
	}
	else
	{
		/* 相对路径相对于调用文件的目录 */
		fc_inc_fullname = (char *) palloc(strlen(fc_outer_filename) + 1 +
									   strlen(fc_inc_filename) + 1);
		strcpy(fc_inc_fullname, fc_outer_filename);
		get_parent_directory(fc_inc_fullname);
		join_path_components(fc_inc_fullname, fc_inc_fullname, fc_inc_filename);
		canonicalize_path(fc_inc_fullname);
	}

	fc_inc_file = AllocateFile(fc_inc_fullname, "r");
	if (fc_inc_file == NULL)
	{
		int			fc_save_errno = errno;

		ereport(fc_elevel,
				(errcode_for_file_access(),
				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
						fc_inc_filename, fc_inc_fullname)));
		*fc_err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
							fc_inc_filename, fc_inc_fullname, strerror(fc_save_errno));
		pfree(fc_inc_fullname);
		return fc_tokens;
	}

	/* 如果文件包含 @，可能会发生递归 */
	fc_linecxt = tokenize_auth_file(fc_inc_fullname, fc_inc_file, &fc_inc_lines, fc_elevel);

	FreeFile(fc_inc_file);
	pfree(fc_inc_fullname);

	/* 复制在文件中找到的所有令牌并附加到令牌列表 */
	foreach(fc_inc_line, fc_inc_lines)
	{
		TokenizedAuthLine *fc_tok_line = (TokenizedAuthLine *) lfirst(fc_inc_line);
		ListCell   *fc_inc_field;

		/* 如果任何行有错误，将其传播给调用者 */
		if (fc_tok_line->err_msg)
		{
			*fc_err_msg = pstrdup(fc_tok_line->err_msg);
			break;
		}

		foreach(fc_inc_field, fc_tok_line->fields)
		{
			List	   *fc_inc_tokens = lfirst(fc_inc_field);
			ListCell   *fc_inc_token;

			foreach(fc_inc_token, fc_inc_tokens)
			{
				AuthToken  *fc_token = lfirst(fc_inc_token);

				fc_tokens = lappend(fc_tokens, fc_copy_auth_token(fc_token));
			}
		}
	}

	MemoryContextDelete(fc_linecxt);
	return fc_tokens;
}

/*
 * tokenize_auth_file
 *		标记化给定文件。
 *
 * 输出是一个 TokenizedAuthLine 结构体的列表；请参见 libpq/hba.h 中的结构定义。
 *
 * filename: 目标文件的绝对路径
 * file: 已打开的目标文件
 * tok_lines: 接收输出列表
 * elevel: 消息日志级别
 *
 * 通过在 ereport 级别 elevel 记录消息以及向输出列表中添加包含非空 err_msg 字段的 TokenizedAuthLine 结构体来报告错误。
 *
 * 返回值是一个内存上下文，其中包含此函数分配的所有内存（它是调用者上下文的子上下文）。
 */
MemoryContext tokenize_auth_file(const char *fc_filename, FILE *fc_file, List **fc_tok_lines,
				   int fc_elevel)
{
	int			fc_line_number = 1;
	StringInfoData fc_buf;
	MemoryContext fc_linecxt;
	MemoryContext fc_oldcxt;

	fc_linecxt = AllocSetContextCreate(CurrentMemoryContext,
									"tokenize_auth_file",
									ALLOCSET_SMALL_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_linecxt);

	initStringInfo(&fc_buf);

	*fc_tok_lines = NIL;

	while (!feof(fc_file) && !ferror(fc_file))
	{
		char	   *fc_lineptr;
		List	   *fc_current_line = NIL;
		char	   *fc_err_msg = NULL;
		int			fc_last_backslash_buflen = 0;
		int			fc_continuations = 0;

		/* 收集下一输入行，处理反斜杠续行 */
		resetStringInfo(&fc_buf);

		while (pg_get_line_append(fc_file, &fc_buf, NULL))
		{
			/* 去掉尾部的换行符，包括在 Windows 上的 \r */
			fc_buf.len = pg_strip_crlf(fc_buf.data);

			/*
			 * 检查反斜杠续行。反斜杠必须位于
			 * 我们找到续行的最后位置之后，否则两个反斜杠
			 * 后跟两个\n将表现得令人惊讶。
			 */
			if (fc_buf.len > fc_last_backslash_buflen &&
				fc_buf.data[fc_buf.len - 1] == '\\')
			{
				/* 续行，因此删除它并继续读取 */
				fc_buf.data[--fc_buf.len] = '\0';
				fc_last_backslash_buflen = fc_buf.len;
				fc_continuations++;
				continue;
			}

			/* 不行，因此我们有整个行 */
			break;
		}

		if (ferror(fc_file))
		{
			/* I/O 错误！ */
			int			fc_save_errno = errno;

			ereport(fc_elevel,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m", fc_filename)));
			fc_err_msg = psprintf("could not read file \"%s\": %s",
							   fc_filename, strerror(fc_save_errno));
			break;
		}

		/* 解析字段 */
		fc_lineptr = fc_buf.data;
		while (*fc_lineptr && fc_err_msg == NULL)
		{
			List	   *fc_current_field;

			fc_current_field = fc_next_field_expand(fc_filename, &fc_lineptr,
											  fc_elevel, &fc_err_msg);
			/* 添加字段到行，除非我们在行尾或注释开始处 */
			if (fc_current_field != NIL)
				fc_current_line = lappend(fc_current_line, fc_current_field);
		}

		/*
		 * 达到行尾；除非这很无聊，否则将行输出到 TokenizedAuthLine 列表
		 */
		if (fc_current_line != NIL || fc_err_msg != NULL)
		{
			TokenizedAuthLine *fc_tok_line;

			fc_tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
			fc_tok_line->fields = fc_current_line;
			fc_tok_line->line_num = fc_line_number;
			fc_tok_line->raw_line = pstrdup(fc_buf.data);
			fc_tok_line->err_msg = fc_err_msg;
			*fc_tok_lines = lappend(*fc_tok_lines, fc_tok_line);
		}

		fc_line_number += fc_continuations + 1;
	}

	MemoryContextSwitchTo(fc_oldcxt);

	return fc_linecxt;
}


/*
 * 用户属于角色吗？
 *
 * userid 是作为尝试登录标识的角色的 OID。
 * 我们检查它是否是指定角色名称的成员。
 */
static bool fc_is_member(Oid fc_userid, const char *fc_role)
{
	Oid			fc_roleid;

	if (!OidIsValid(fc_userid))
		return false;			/* 如果用户不存在，返回 "no" */

	fc_roleid = get_role_oid(fc_role, true);

	if (!OidIsValid(fc_roleid))
		return false;			/* 如果目标角色不存在，返回 "no" */

	/*
	 * 查看用户是否直接或间接是角色的成员。为此
	 * 目的，超级用户不会被视为自动成员，
	 * 因此组身份验证仅适用于明确的成员资格。
	 */
	return is_member_of_role_nosuper(fc_userid, fc_roleid);
}

/*
 * 检查 AuthToken 列表是否与角色匹配，允许组名称。
 */
static bool fc_check_role(const char *fc_role, Oid fc_roleid, List *fc_tokens)
{
	ListCell   *fc_cell;
	AuthToken  *fc_tok;

	foreach(fc_cell, fc_tokens)
	{
		fc_tok = lfirst(fc_cell);
		if (!fc_tok->quoted && fc_tok->string[0] == '+')
		{
			if (fc_is_member(fc_roleid, fc_tok->string + 1))
				return true;
		}
		else if (token_matches(fc_tok, fc_role) ||
				 token_is_keyword(fc_tok, "all"))
			return true;
	}
	return false;
}

/*
 * 检查 db/role 组合是否与 AuthToken 列表匹配。
 */
static bool fc_check_db(const char *fc_dbname, const char *fc_role, Oid fc_roleid, List *fc_tokens)
{
	ListCell   *fc_cell;
	AuthToken  *fc_tok;

	foreach(fc_cell, fc_tokens)
	{
		fc_tok = lfirst(fc_cell);
		if (am_walsender && !am_db_walsender)
		{
			/*
			 * 物理复制 walsender 连接只能匹配
			 * 复制关键字
			 */
			if (token_is_keyword(fc_tok, "replication"))
				return true;
		}
		else if (token_is_keyword(fc_tok, "all"))
			return true;
		else if (token_is_keyword(fc_tok, "sameuser"))
		{
			if (strcmp(fc_dbname, fc_role) == 0)
				return true;
		}
		else if (token_is_keyword(fc_tok, "samegroup") ||
				 token_is_keyword(fc_tok, "samerole"))
		{
			if (fc_is_member(fc_roleid, fc_dbname))
				return true;
		}
		else if (token_is_keyword(fc_tok, "replication"))
			continue;			/* 如果不是 walsender，绝不会匹配此项 */
		else if (token_matches(fc_tok, fc_dbname))
			return true;
	}
	return false;
}

static bool fc_ipv4eq(struct sockaddr_in *fc_a, struct sockaddr_in *fc_b)
{
	return (fc_a->sin_addr.s_addr == fc_b->sin_addr.s_addr);
}

#ifdef HAVE_IPV6

static bool fc_ipv6eq(struct sockaddr_in6 *fc_a, struct sockaddr_in6 *fc_b)
{
	int			fc_i;

	for (fc_i = 0; fc_i < 16; fc_i++)
		if (fc_a->sin6_addr.s6_addr[fc_i] != fc_b->sin6_addr.s6_addr[fc_i])
			return false;

	return true;
}
#endif							/* HAVE_IPV6 */

/*
 * 检查主机名是否匹配模式。
 */
static bool fc_hostname_match(const char *fc_pattern, const char *fc_actual_hostname)
{
	if (fc_pattern[0] == '.')		/* 后缀匹配 */
	{
		size_t		fc_plen = strlen(fc_pattern);
		size_t		fc_hlen = strlen(fc_actual_hostname);

		if (fc_hlen < fc_plen)
			return false;

		return (pg_strcasecmp(fc_pattern, fc_actual_hostname + (fc_hlen - fc_plen)) == 0);
	}
	else
		return (pg_strcasecmp(fc_pattern, fc_actual_hostname) == 0);
}

/*
 * 检查连接的 IP 是否与给定主机名匹配。
 */
static bool fc_check_hostname(hbaPort *fc_port, const char *fc_hostname)
{
	struct addrinfo *fc_gai_result,
			   *fc_gai;
	int			fc_ret;
	bool		fc_found;

	/* 如果远程主机名已经知道为坏，快速退出 */
	if (fc_port->remote_hostname_resolv < 0)
		return false;

	/* 如果尚未完成，则查找远程主机名 */
	if (!fc_port->remote_hostname)
	{
		char		fc_remote_hostname[NI_MAXHOST];

		fc_ret = pg_getnameinfo_all(&fc_port->raddr.addr, fc_port->raddr.salen,
								 fc_remote_hostname, sizeof(fc_remote_hostname),
								 NULL, 0,
								 NI_NAMEREQD);
		if (fc_ret != 0)
		{
			/* 记住失败；尚不在 postmaster 日志中抱怨 */
			fc_port->remote_hostname_resolv = -2;
			fc_port->remote_hostname_errcode = fc_ret;
			return false;
		}

		fc_port->remote_hostname = pstrdup(fc_remote_hostname);
	}

	/* 现在查看远程主机名是否与此 pg_hba 行匹配 */
	if (!fc_hostname_match(fc_hostname, fc_port->remote_hostname))
		return false;

	/* 如果我们已经验证了前向查找，我们就完成了 */
	if (fc_port->remote_hostname_resolv == +1)
		return true;

	/* 从主机名查找 IP，并与原始 IP 进行检查 */
	fc_ret = getaddrinfo(fc_port->remote_hostname, NULL, NULL, &fc_gai_result);
	if (fc_ret != 0)
	{
		/* 记住失败；尚不在 postmaster 日志中抱怨 */
		fc_port->remote_hostname_resolv = -2;
		fc_port->remote_hostname_errcode = fc_ret;
		return false;
	}

	fc_found = false;
	for (fc_gai = fc_gai_result; fc_gai; fc_gai = fc_gai->ai_next)
	{
		if (fc_gai->ai_addr->sa_family == fc_port->raddr.addr.ss_family)
		{
			if (fc_gai->ai_addr->sa_family == AF_INET)
			{
				if (fc_ipv4eq((struct sockaddr_in *) fc_gai->ai_addr,
						   (struct sockaddr_in *) &fc_port->raddr.addr))
				{
					fc_found = true;
					break;
				}
			}
#ifdef HAVE_IPV6
			else if (fc_gai->ai_addr->sa_family == AF_INET6)
			{
				if (fc_ipv6eq((struct sockaddr_in6 *) fc_gai->ai_addr,
						   (struct sockaddr_in6 *) &fc_port->raddr.addr))
				{
					fc_found = true;
					break;
				}
			}
#endif
		}
	}

	if (fc_gai_result)
		freeaddrinfo(fc_gai_result);

	if (!fc_found)
		elog(DEBUG2, "pg_hba.conf host name \"%s\" rejected because address resolution did not return a match with IP address of client",
			 fc_hostname);

	fc_port->remote_hostname_resolv = fc_found ? +1 : -1;

	return fc_found;
}

/*
 * 检查连接的 IP 是否与给定地址和网络掩码匹配。
 */
static bool fc_check_ip(SockAddr *fc_raddr, struct sockaddr *fc_addr, struct sockaddr *fc_mask)
{
	if (fc_raddr->addr.ss_family == fc_addr->sa_family &&
		pg_range_sockaddr(&fc_raddr->addr,
						  (struct sockaddr_storage *) fc_addr,
						  (struct sockaddr_storage *) fc_mask))
		return true;
	return false;
}

/*
 * pg_foreach_ifaddr 回调：客户端地址是否与此机器接口匹配？
 */
static void fc_check_network_callback(struct sockaddr *fc_addr, struct sockaddr *fc_netmask,
					   void *fc_cb_data)
{
	check_network_data *fc_cn = (check_network_data *) fc_cb_data;
	struct sockaddr_storage fc_mask;

	/* 已经找到匹配项了吗？ */
	if (fc_cn->result)
		return;

	if (fc_cn->method == ipCmpSameHost)
	{
		/* 为适当的长度和类别制作全一的网络掩码 */
		pg_sockaddr_cidr_mask(&fc_mask, NULL, fc_addr->sa_family);
		fc_cn->result = fc_check_ip(fc_cn->raddr, fc_addr, (struct sockaddr *) &fc_mask);
	}
	else
	{
		/* 使用接口本身的网络掩码 */
		fc_cn->result = fc_check_ip(fc_cn->raddr, fc_addr, fc_netmask);
	}
}

/*
 * 使用 pg_foreach_ifaddr 检查同主机或同网络匹配
 */
static bool fc_check_same_host_or_net(SockAddr *fc_raddr, IPCompareMethod fc_method)
{
	check_network_data fc_cn;

	fc_cn.method = fc_method;
	fc_cn.raddr = fc_raddr;
	fc_cn.result = false;

	errno = 0;
	if (pg_foreach_ifaddr(fc_check_network_callback, &fc_cn) < 0)
	{
		ereport(LOG,
				(errmsg("error enumerating network interfaces: %m")));
		return false;
	}

	return fc_cn.result;
}


/*
 * 用于检查和报告无效配置选项的宏。
 * 出错时：在elevel级别记录消息，设置*err_msg，并退出函数。
 * 这些宏并不像看起来那么通用，因为它们知道调用函数的错误退出值。
 *
 * INVALID_AUTH_OPTION = 当为不支持的方法指定选项时报告。
 * REQUIRE_AUTH_OPTION = 与INVALID_AUTH_OPTION相同，除了它还检查
 *						   方法是否确实是指定的。在选项只对一种身份验证方法有效时
 *						   作为快捷方式使用。
 * MANDATORY_AUTH_ARG  = 检查身份验证方法是否设置了必需选项，
 *						   如果未设置则报告错误。
 */
#define INVALID_AUTH_OPTION(optname, validmethods) \
do { \
	ereport(fc_elevel, \
			(errcode(ERRCODE_CONFIG_FILE_ERROR), \
			 /* 翻译者：第二个%s是身份验证方法的列表 */ \
			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
					optname, _(validmethods)), \
			 errcontext("line %d of configuration file \"%s\"", \
					fc_line_num, HbaFileName))); \
	*fc_err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
						optname, validmethods); \
	return false; \
} while (0)

#define REQUIRE_AUTH_OPTION(methodval, optname, validmethods) \
do { \
	if (fc_hbaline->auth_method != methodval) \
		INVALID_AUTH_OPTION(optname, validmethods); \
} while (0)

#define MANDATORY_AUTH_ARG(argvar, argname, authname) \
do { \
	if (argvar == NULL) { \
		ereport(fc_elevel, \
				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
						authname, argname), \
				 errcontext("line %d of configuration file \"%s\"", \
						fc_line_num, HbaFileName))); \
		*fc_err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
							authname, argname); \
		return NULL; \
	} \
} while (0)

/*
 * 处理pg_ident问题的宏，类似于上述。
 *
 * IDENT_FIELD_ABSENT:
 * 当给定的ident字段ListCell未填充时报告。
 *
 * IDENT_MULTI_VALUE:
 * 当给定的ident令牌List包含多个元素时报告。
 */
#define IDENT_FIELD_ABSENT(field) \
do { \
	if (!field) { \
		ereport(fc_elevel, \
				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
				 errmsg("missing entry in file \"%s\" at end of line %d", \
						IdentFileName, fc_line_num))); \
		*fc_err_msg = psprintf("missing entry at end of line"); \
		return NULL; \
	} \
} while (0)

#define IDENT_MULTI_VALUE(tokens) \
do { \
	if (tokens->length > 1) { \
		ereport(fc_elevel, \
				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
				 errmsg("multiple values in ident field"), \
				 errcontext("line %d of configuration file \"%s\"", \
							fc_line_num, IdentFileName))); \
		*fc_err_msg = psprintf("multiple values in ident field"); \
		return NULL; \
	} \
} while (0)


/*
 * 解析hba配置文件中的一个令牌化行，并将结果存储在
 * HbaLine结构中。
 *
 * 如果解析失败，在ereport级别elevel记录消息，将错误字符串
 * 存储在tok_line->err_msg中，并返回NULL。 （一些非错误条件
 * 也可能导致此类消息。）
 *
 * 注意：此函数在发生错误时会造成内存泄漏。调用者应预期
 * 设置一个内存上下文，如果此函数返回NULL，将重置该上下文。
 */
HbaLine *
parse_hba_line(TokenizedAuthLine *fc_tok_line, int fc_elevel)
{
	int			fc_line_num = fc_tok_line->line_num;
	char	  **fc_err_msg = &fc_tok_line->err_msg;
	char	   *fc_str;
	struct addrinfo *fc_gai_result;
	struct addrinfo fc_hints;
	int			fc_ret;
	char	   *fc_cidr_slash;
	char	   *fc_unsupauth;
	ListCell   *fc_field;
	List	   *fc_tokens;
	ListCell   *fc_tokencell;
	AuthToken  *fc_token;
	HbaLine    *fc_parsedline;

	fc_parsedline = palloc0(sizeof(HbaLine));
	fc_parsedline->linenumber = fc_line_num;
	fc_parsedline->rawline = pstrdup(fc_tok_line->raw_line);

	/* 检查记录类型。 */
	Assert(fc_tok_line->fields != NIL);
	fc_field = list_head(fc_tok_line->fields);
	fc_tokens = lfirst(fc_field);
	if (fc_tokens->length > 1)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("multiple values specified for connection type"),
				 errhint("Specify exactly one connection type per line."),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "multiple values specified for connection type";
		return NULL;
	}
	fc_token = linitial(fc_tokens);
	if (strcmp(fc_token->string, "local") == 0)
	{
#ifdef HAVE_UNIX_SOCKETS
		fc_parsedline->conntype = ctLocal;
#else
		ereport(elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("local connections are not supported by this build"),
				 errcontext("line %d of configuration file \"%s\"",
							line_num, HbaFileName)));
		*fc_err_msg = "local connections are not supported by this build";
		return NULL;
#endif
	}
	else if (strcmp(fc_token->string, "host") == 0 ||
			 strcmp(fc_token->string, "hostssl") == 0 ||
			 strcmp(fc_token->string, "hostnossl") == 0 ||
			 strcmp(fc_token->string, "hostgssenc") == 0 ||
			 strcmp(fc_token->string, "hostnogssenc") == 0)
	{

		if (fc_token->string[4] == 's')	/* "hostssl" */
		{
			fc_parsedline->conntype = ctHostSSL;
			/* 如果SSL支持未激活则记录警告 */
#ifdef USE_SSL
			if (!EnableSSL)
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("hostssl record cannot match because SSL is disabled"),
						 errhint("Set ssl = on in postgresql.conf."),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				*fc_err_msg = "hostssl record cannot match because SSL is disabled";
			}
#else
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "hostssl record cannot match because SSL is not supported by this build";
#endif
		}
		else if (fc_token->string[4] == 'g')	/* "hostgssenc" */
		{
			fc_parsedline->conntype = ctHostGSS;
#ifndef ENABLE_GSS
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
#endif
		}
		else if (fc_token->string[4] == 'n' && fc_token->string[6] == 's')
			fc_parsedline->conntype = ctHostNoSSL;
		else if (fc_token->string[4] == 'n' && fc_token->string[6] == 'g')
			fc_parsedline->conntype = ctHostNoGSS;
		else
		{
			/* "host" */
			fc_parsedline->conntype = ctHost;
		}
	}							/* 记录类型 */
	else
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("invalid connection type \"%s\"",
						fc_token->string),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = psprintf("invalid connection type \"%s\"", fc_token->string);
		return NULL;
	}

	/* 获取数据库。 */
	fc_field = lnext(fc_tok_line->fields, fc_field);
	if (!fc_field)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("end-of-line before database specification"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "end-of-line before database specification";
		return NULL;
	}
	fc_parsedline->databases = NIL;
	fc_tokens = lfirst(fc_field);
	foreach(fc_tokencell, fc_tokens)
	{
		fc_parsedline->databases = lappend(fc_parsedline->databases,
										fc_copy_auth_token(lfirst(fc_tokencell)));
	}

	/* 获取角色。 */
	fc_field = lnext(fc_tok_line->fields, fc_field);
	if (!fc_field)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("end-of-line before role specification"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "end-of-line before role specification";
		return NULL;
	}
	fc_parsedline->roles = NIL;
	fc_tokens = lfirst(fc_field);
	foreach(fc_tokencell, fc_tokens)
	{
		fc_parsedline->roles = lappend(fc_parsedline->roles,
									fc_copy_auth_token(lfirst(fc_tokencell)));
	}

	if (fc_parsedline->conntype != ctLocal)
	{
		/* 读取IP地址字段。 （带或不带CIDR子网掩码） */
		fc_field = lnext(fc_tok_line->fields, fc_field);
		if (!fc_field)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("end-of-line before IP address specification"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "end-of-line before IP address specification";
			return NULL;
		}
		fc_tokens = lfirst(fc_field);
		if (fc_tokens->length > 1)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("multiple values specified for host address"),
					 errhint("Specify one address range per line."),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "multiple values specified for host address";
			return NULL;
		}
		fc_token = linitial(fc_tokens);

		if (token_is_keyword(fc_token, "all"))
		{
			fc_parsedline->ip_cmp_method = ipCmpAll;
		}
		else if (token_is_keyword(fc_token, "samehost"))
		{
			/* 允许此主机上的任何IP连接 */
			fc_parsedline->ip_cmp_method = ipCmpSameHost;
		}
		else if (token_is_keyword(fc_token, "samenet"))
		{
			/* 允许主机子网上的任何IP连接 */
			fc_parsedline->ip_cmp_method = ipCmpSameNet;
		}
		else
		{
			/* 指定了IP和子网掩码 */
			fc_parsedline->ip_cmp_method = ipCmpMask;

			/* 需要可修改的令牌副本 */
			fc_str = pstrdup(fc_token->string);

			/* 检查它是否具有CIDR后缀，如果有，则隔离它 */
			fc_cidr_slash = strchr(fc_str, '/');
			if (fc_cidr_slash)
				*fc_cidr_slash = '\0';

			/* 以任何方式获取IP地址 */
			fc_hints.ai_flags = AI_NUMERICHOST;
			fc_hints.ai_family = AF_UNSPEC;
			fc_hints.ai_socktype = 0;
			fc_hints.ai_protocol = 0;
			fc_hints.ai_addrlen = 0;
			fc_hints.ai_canonname = NULL;
			fc_hints.ai_addr = NULL;
			fc_hints.ai_next = NULL;

			fc_ret = pg_getaddrinfo_all(fc_str, NULL, &fc_hints, &fc_gai_result);
			if (fc_ret == 0 && fc_gai_result)
			{
				memcpy(&fc_parsedline->addr, fc_gai_result->ai_addr,
					   fc_gai_result->ai_addrlen);
				fc_parsedline->addrlen = fc_gai_result->ai_addrlen;
			}
			else if (fc_ret == EAI_NONAME)
				fc_parsedline->hostname = fc_str;
			else
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("invalid IP address \"%s\": %s",
								fc_str, gai_strerror(fc_ret)),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				*fc_err_msg = psprintf("invalid IP address \"%s\": %s",
									fc_str, gai_strerror(fc_ret));
				if (fc_gai_result)
					pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);
				return NULL;
			}

			pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);

			/* 获取子网掩码 */
			if (fc_cidr_slash)
			{
				if (fc_parsedline->hostname)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
									fc_token->string),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
										fc_token->string);
					return NULL;
				}

				if (pg_sockaddr_cidr_mask(&fc_parsedline->mask, fc_cidr_slash + 1,
										  fc_parsedline->addr.ss_family) < 0)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("invalid CIDR mask in address \"%s\"",
									fc_token->string),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = psprintf("invalid CIDR mask in address \"%s\"",
										fc_token->string);
					return NULL;
				}
				fc_parsedline->masklen = fc_parsedline->addrlen;
				pfree(fc_str);
			}
			else if (!fc_parsedline->hostname)
			{
				/* 读取掩码字段。 */
				pfree(fc_str);
				fc_field = lnext(fc_tok_line->fields, fc_field);
				if (!fc_field)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("end-of-line before netmask specification"),
							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = "end-of-line before netmask specification";
					return NULL;
				}
				fc_tokens = lfirst(fc_field);
				if (fc_tokens->length > 1)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("multiple values specified for netmask"),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = "multiple values specified for netmask";
					return NULL;
				}
				fc_token = linitial(fc_tokens);

				fc_ret = pg_getaddrinfo_all(fc_token->string, NULL,
										 &fc_hints, &fc_gai_result);
				if (fc_ret || !fc_gai_result)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("invalid IP mask \"%s\": %s",
									fc_token->string, gai_strerror(fc_ret)),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = psprintf("invalid IP mask \"%s\": %s",
										fc_token->string, gai_strerror(fc_ret));
					if (fc_gai_result)
						pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);
					return NULL;
				}

				memcpy(&fc_parsedline->mask, fc_gai_result->ai_addr,
					   fc_gai_result->ai_addrlen);
				fc_parsedline->masklen = fc_gai_result->ai_addrlen;
				pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);

				if (fc_parsedline->addr.ss_family != fc_parsedline->mask.ss_family)
				{
					ereport(fc_elevel,
							(errcode(ERRCODE_CONFIG_FILE_ERROR),
							 errmsg("IP address and mask do not match"),
							 errcontext("line %d of configuration file \"%s\"",
										fc_line_num, HbaFileName)));
					*fc_err_msg = "IP address and mask do not match";
					return NULL;
				}
			}
		}
	}							/* != ctLocal */

	/* 获取认证方法 */
	fc_field = lnext(fc_tok_line->fields, fc_field);
	if (!fc_field)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("end-of-line before authentication method"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "end-of-line before authentication method";
		return NULL;
	}
	fc_tokens = lfirst(fc_field);
	if (fc_tokens->length > 1)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("multiple values specified for authentication type"),
				 errhint("Specify exactly one authentication type per line."),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "multiple values specified for authentication type";
		return NULL;
	}
	fc_token = linitial(fc_tokens);

	fc_unsupauth = NULL;
	if (strcmp(fc_token->string, "trust") == 0)
		fc_parsedline->auth_method = uaTrust;
	else if (strcmp(fc_token->string, "ident") == 0)
		fc_parsedline->auth_method = uaIdent;
	else if (strcmp(fc_token->string, "peer") == 0)
		fc_parsedline->auth_method = uaPeer;
	else if (strcmp(fc_token->string, "password") == 0)
		fc_parsedline->auth_method = uaPassword;
	else if (strcmp(fc_token->string, "gss") == 0)
#ifdef ENABLE_GSS
		fc_parsedline->auth_method = uaGSS;
#else
		fc_unsupauth = "gss";
#endif
	else if (strcmp(fc_token->string, "sspi") == 0)
#ifdef ENABLE_SSPI
		fc_parsedline->auth_method = uaSSPI;
#else
		fc_unsupauth = "sspi";
#endif
	else if (strcmp(fc_token->string, "reject") == 0)
		fc_parsedline->auth_method = uaReject;
	else if (strcmp(fc_token->string, "md5") == 0)
	{
		if (Db_user_namespace)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
			return NULL;
		}
		fc_parsedline->auth_method = uaMD5;
	}
#ifdef FDDGM
	else if (strcmp(fc_token->string, "sm3") == 0) 
		fc_parsedline->auth_method = uaSM3;
#endif
	else if (strcmp(fc_token->string, "scram-sha-256") == 0)
		fc_parsedline->auth_method = uaSCRAM;
	else if (strcmp(fc_token->string, "pam") == 0)
#ifdef USE_PAM
		fc_parsedline->auth_method = uaPAM;
#else
		fc_unsupauth = "pam";
#endif
	else if (strcmp(fc_token->string, "bsd") == 0)
#ifdef USE_BSD_AUTH
		fc_parsedline->auth_method = uaBSD;
#else
		fc_unsupauth = "bsd";
#endif
	else if (strcmp(fc_token->string, "ldap") == 0)
#ifdef USE_LDAP
		fc_parsedline->auth_method = uaLDAP;
#else
		fc_unsupauth = "ldap";
#endif
	else if (strcmp(fc_token->string, "cert") == 0)
#ifdef USE_SSL
		fc_parsedline->auth_method = uaCert;
#else
		fc_unsupauth = "cert";
#endif
	else if (strcmp(fc_token->string, "radius") == 0)
		fc_parsedline->auth_method = uaRADIUS;
	else
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("invalid authentication method \"%s\"",
						fc_token->string),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = psprintf("invalid authentication method \"%s\"",
							fc_token->string);
		return NULL;
	}

	if (fc_unsupauth)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("invalid authentication method \"%s\": not supported by this build",
						fc_token->string),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
							fc_token->string);
		return NULL;
	}

	/*
	 * XXX：在本地连接上使用ident时，将其更改为peer，以
	 * 便于向后兼容。
	 */
	if (fc_parsedline->conntype == ctLocal &&
		fc_parsedline->auth_method == uaIdent)
		fc_parsedline->auth_method = uaPeer;

	/* 无效的身份验证组合 */
	if (fc_parsedline->conntype == ctLocal &&
		fc_parsedline->auth_method == uaGSS)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("gssapi authentication is not supported on local sockets"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "gssapi authentication is not supported on local sockets";
		return NULL;
	}

	if (fc_parsedline->conntype != ctLocal &&
		fc_parsedline->auth_method == uaPeer)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("peer authentication is only supported on local sockets"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "peer authentication is only supported on local sockets";
		return NULL;
	}

	/*
	 * SSPI身份验证永远无法在ctLocal连接上启用，
	 * 因为它仅支持Windows，而ctLocal不支持。
	 */


	if (fc_parsedline->conntype != ctHostSSL &&
		fc_parsedline->auth_method == uaCert)
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("cert authentication is only supported on hostssl connections"),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = "cert authentication is only supported on hostssl connections";
		return NULL;
	}

	/*
	 * 对于 GSS 和 SSPI，将 include_realm 的默认值设置为 true。
	 * 在多领域情况下，将 include_realm 设置为 false 是危险的，
	 * 并且通常被视为不良实践。我们保留这一能力以便向后兼容，但
	 * 未来某个时候可能会想要移除它。仍然需要去掉领域的用户
	 * 使用 pg_ident.conf 映射中的适当正则表达式会更好。
	 */
	if (fc_parsedline->auth_method == uaGSS ||
		fc_parsedline->auth_method == uaSSPI)
		fc_parsedline->include_realm = true;

	/*
	 * 对于 SSPI，include_realm 默认为与 SAM 兼容的域（即 NetBIOS 名称）
	 * 和用户名，而不是 Kerberos 主体名称，以保持兼容性。
	 */
	if (fc_parsedline->auth_method == uaSSPI)
	{
		fc_parsedline->compat_realm = true;
		fc_parsedline->upn_username = false;
	}

	/* 解析剩余参数 */
	while ((fc_field = lnext(fc_tok_line->fields, fc_field)) != NULL)
	{
		fc_tokens = lfirst(fc_field);
		foreach(fc_tokencell, fc_tokens)
		{
			char	   *fc_val;

			fc_token = lfirst(fc_tokencell);

			fc_str = pstrdup(fc_token->string);
			fc_val = strchr(fc_str, '=');
			if (fc_val == NULL)
			{
				/*
				 * 得到了一个不是 name=value 对的东西。
				 */
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("authentication option not in name=value format: %s", fc_token->string),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				*fc_err_msg = psprintf("authentication option not in name=value format: %s",
									fc_token->string);
				return NULL;
			}

			*fc_val++ = '\0';		/* str 现在保存 "name"，val 保存 "value" */
			if (!fc_parse_hba_auth_opt(fc_str, fc_val, fc_parsedline, fc_elevel, fc_err_msg))
				/* parse_hba_auth_opt 已经记录了错误信息 */
				return NULL;
			pfree(fc_str);
		}
	}

	/*
	 * 检查所选认证方法是否有未设置的强制参数。
	 */
	if (fc_parsedline->auth_method == uaLDAP)
	{
#ifndef HAVE_LDAP_INITIALIZE
		/* 对于 OpenLDAP 来说不是强制性的，因为它可以使用 DNS SRV 记录 */
		MANDATORY_AUTH_ARG(fc_parsedline->ldapserver, "ldapserver", "ldap");
#endif

		/*
		 * LDAP 可以在两种模式下运行：直接绑定，使用 ldapprefix 和
		 * ldapsuffix，或者使用搜索+绑定，使用 ldapbasedn、
		 * ldapbinddn、ldapbindpasswd 和 ldapsearchattribute 或
		 * ldapsearchfilter 之一。禁止混合使用这些参数。
		 */
		if (fc_parsedline->ldapprefix || fc_parsedline->ldapsuffix)
		{
			if (fc_parsedline->ldapbasedn ||
				fc_parsedline->ldapbinddn ||
				fc_parsedline->ldapbindpasswd ||
				fc_parsedline->ldapsearchattribute ||
				fc_parsedline->ldapsearchfilter)
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				*fc_err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
				return NULL;
			}
		}
		else if (!fc_parsedline->ldapbasedn)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
			return NULL;
		}

		/*
		 * 当使用搜索+绑定时，可以使用简单属性（默认 "uid"）或完全
		 * 自定义的搜索过滤器。两者不能同时使用。
		 */
		if (fc_parsedline->ldapsearchattribute && fc_parsedline->ldapsearchfilter)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
			return NULL;
		}
	}

	if (fc_parsedline->auth_method == uaRADIUS)
	{
		MANDATORY_AUTH_ARG(fc_parsedline->radiusservers, "radiusservers", "radius");
		MANDATORY_AUTH_ARG(fc_parsedline->radiussecrets, "radiussecrets", "radius");

		if (list_length(fc_parsedline->radiusservers) < 1)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("list of RADIUS servers cannot be empty"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "list of RADIUS servers cannot be empty";
			return NULL;
		}

		if (list_length(fc_parsedline->radiussecrets) < 1)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("list of RADIUS secrets cannot be empty"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "list of RADIUS secrets cannot be empty";
			return NULL;
		}

		/*
		 * 验证选项列表的长度 - 每个选项可以是 0（除了密码，
		 * 但在上面已检查）、1（在所有地方使用相同值）或与服务器数量相同。
		 */
		if (!(list_length(fc_parsedline->radiussecrets) == 1 ||
			  list_length(fc_parsedline->radiussecrets) == list_length(fc_parsedline->radiusservers)))
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
							list_length(fc_parsedline->radiussecrets),
							list_length(fc_parsedline->radiusservers)),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
								list_length(fc_parsedline->radiussecrets),
								list_length(fc_parsedline->radiusservers));
			return NULL;
		}
		if (!(list_length(fc_parsedline->radiusports) == 0 ||
			  list_length(fc_parsedline->radiusports) == 1 ||
			  list_length(fc_parsedline->radiusports) == list_length(fc_parsedline->radiusservers)))
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
							list_length(fc_parsedline->radiusports),
							list_length(fc_parsedline->radiusservers)),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
								list_length(fc_parsedline->radiusports),
								list_length(fc_parsedline->radiusservers));
			return NULL;
		}
		if (!(list_length(fc_parsedline->radiusidentifiers) == 0 ||
			  list_length(fc_parsedline->radiusidentifiers) == 1 ||
			  list_length(fc_parsedline->radiusidentifiers) == list_length(fc_parsedline->radiusservers)))
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
							list_length(fc_parsedline->radiusidentifiers),
							list_length(fc_parsedline->radiusservers)),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
								list_length(fc_parsedline->radiusidentifiers),
								list_length(fc_parsedline->radiusservers));
			return NULL;
		}
	}

	/*
	 * 强制执行由其他设置隐含的任何参数。
	 */
	if (fc_parsedline->auth_method == uaCert)
	{
		/*
		 * 对于认证方法 cert，客户端证书验证是强制性的，
		 * 并且它意味着全验证级别。
		 */
		fc_parsedline->clientcert = clientCertFull;
	}

	return fc_parsedline;
}


/*
 * 将一个名称-值对作为认证选项解析到给定的 HbaLine 中。
 * 如果我们成功解析该选项，则返回 true，若遇到错误则返回 false。
 * 如果发生错误，还需在 ereport 级别 elevel 记录一条消息，
 * 并将消息字符串存储到 *err_msg 中。
 */
static bool fc_parse_hba_auth_opt(char *fc_name, char *fc_val, HbaLine *fc_hbaline,
				   int fc_elevel, char **fc_err_msg)
{
	int			fc_line_num = fc_hbaline->linenumber;

#ifdef USE_LDAP
	fc_hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
#endif

	if (strcmp(fc_name, "map") == 0)
	{
		if (fc_hbaline->auth_method != uaIdent &&
			fc_hbaline->auth_method != uaPeer &&
			fc_hbaline->auth_method != uaGSS &&
			fc_hbaline->auth_method != uaSSPI &&
			fc_hbaline->auth_method != uaCert)
			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
		fc_hbaline->usermap = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "clientcert") == 0)
	{
		if (fc_hbaline->conntype != ctHostSSL)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "clientcert can only be configured for \"hostssl\" rows";
			return false;
		}

		if (strcmp(fc_val, "verify-full") == 0)
		{
			fc_hbaline->clientcert = clientCertFull;
		}
		else if (strcmp(fc_val, "verify-ca") == 0)
		{
			if (fc_hbaline->auth_method == uaCert)
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				*fc_err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
				return false;
			}

			fc_hbaline->clientcert = clientCertCA;
		}
		else
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("invalid value for clientcert: \"%s\"", fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			return false;
		}
	}
	else if (strcmp(fc_name, "clientname") == 0)
	{
		if (fc_hbaline->conntype != ctHostSSL)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("clientname can only be configured for \"hostssl\" rows"),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = "clientname can only be configured for \"hostssl\" rows";
			return false;
		}

		if (strcmp(fc_val, "CN") == 0)
		{
			fc_hbaline->clientcertname = clientCertCN;
		}
		else if (strcmp(fc_val, "DN") == 0)
		{
			fc_hbaline->clientcertname = clientCertDN;
		}
		else
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("invalid value for clientname: \"%s\"", fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			return false;
		}
	}
	else if (strcmp(fc_name, "pamservice") == 0)
	{
		REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");
		fc_hbaline->pamservice = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "pam_use_hostname") == 0)
	{
		REQUIRE_AUTH_OPTION(uaPAM, "pam_use_hostname", "pam");
		if (strcmp(fc_val, "1") == 0)
			fc_hbaline->pam_use_hostname = true;
		else
			fc_hbaline->pam_use_hostname = false;
	}
	else if (strcmp(fc_name, "ldapurl") == 0)
	{
#ifdef LDAP_API_FEATURE_X_OPENLDAP
		LDAPURLDesc *fc_urldata;
		int			fc_rc;
#endif

		REQUIRE_AUTH_OPTION(uaLDAP, "ldapurl", "ldap");
#ifdef LDAP_API_FEATURE_X_OPENLDAP
		fc_rc = ldap_url_parse(fc_val, &fc_urldata);
		if (fc_rc != LDAP_SUCCESS)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("could not parse LDAP URL \"%s\": %s", fc_val, ldap_err2string(fc_rc))));
			*fc_err_msg = psprintf("could not parse LDAP URL \"%s\": %s",
								fc_val, ldap_err2string(fc_rc));
			return false;
		}

		if (strcmp(fc_urldata->lud_scheme, "ldap") != 0 &&
			strcmp(fc_urldata->lud_scheme, "ldaps") != 0)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("unsupported LDAP URL scheme: %s", fc_urldata->lud_scheme)));
			*fc_err_msg = psprintf("unsupported LDAP URL scheme: %s",
								fc_urldata->lud_scheme);
			ldap_free_urldesc(fc_urldata);
			return false;
		}

		if (fc_urldata->lud_scheme)
			fc_hbaline->ldapscheme = pstrdup(fc_urldata->lud_scheme);
		if (fc_urldata->lud_host)
			fc_hbaline->ldapserver = pstrdup(fc_urldata->lud_host);
		fc_hbaline->ldapport = fc_urldata->lud_port;
		if (fc_urldata->lud_dn)
			fc_hbaline->ldapbasedn = pstrdup(fc_urldata->lud_dn);

		if (fc_urldata->lud_attrs)
			fc_hbaline->ldapsearchattribute = pstrdup(fc_urldata->lud_attrs[0]);	/* 只使用第一个 */
		fc_hbaline->ldapscope = fc_urldata->lud_scope;
		if (fc_urldata->lud_filter)
			fc_hbaline->ldapsearchfilter = pstrdup(fc_urldata->lud_filter);
		ldap_free_urldesc(fc_urldata);
#else							/* 不是 OpenLDAP */
		ereport(fc_elevel,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("LDAP URLs not supported on this platform")));
		*fc_err_msg = "LDAP URLs not supported on this platform";
#endif							/* 不是 OpenLDAP */
	}
	else if (strcmp(fc_name, "ldaptls") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap");
		if (strcmp(fc_val, "1") == 0)
			fc_hbaline->ldaptls = true;
		else
			fc_hbaline->ldaptls = false;
	}
	else if (strcmp(fc_name, "ldapscheme") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
		if (strcmp(fc_val, "ldap") != 0 && strcmp(fc_val, "ldaps") != 0)
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("invalid ldapscheme value: \"%s\"", fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
		fc_hbaline->ldapscheme = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapserver") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
		fc_hbaline->ldapserver = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapport") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap");
		fc_hbaline->ldapport = atoi(fc_val);
		if (fc_hbaline->ldapport == 0)
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("invalid LDAP port number: \"%s\"", fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = psprintf("invalid LDAP port number: \"%s\"", fc_val);
			return false;
		}
	}
	else if (strcmp(fc_name, "ldapbinddn") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
		fc_hbaline->ldapbinddn = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapbindpasswd") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
		fc_hbaline->ldapbindpasswd = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapsearchattribute") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
		fc_hbaline->ldapsearchattribute = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapsearchfilter") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
		fc_hbaline->ldapsearchfilter = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapbasedn") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
		fc_hbaline->ldapbasedn = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapprefix") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
		fc_hbaline->ldapprefix = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "ldapsuffix") == 0)
	{
		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap");
		fc_hbaline->ldapsuffix = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "krb_realm") == 0)
	{
		if (fc_hbaline->auth_method != uaGSS &&
			fc_hbaline->auth_method != uaSSPI)
			INVALID_AUTH_OPTION("krb_realm", gettext_noop("gssapi and sspi"));
		fc_hbaline->krb_realm = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "include_realm") == 0)
	{
		if (fc_hbaline->auth_method != uaGSS &&
			fc_hbaline->auth_method != uaSSPI)
			INVALID_AUTH_OPTION("include_realm", gettext_noop("gssapi and sspi"));
		if (strcmp(fc_val, "1") == 0)
			fc_hbaline->include_realm = true;
		else
			fc_hbaline->include_realm = false;
	}
	else if (strcmp(fc_name, "compat_realm") == 0)
	{
		if (fc_hbaline->auth_method != uaSSPI)
			INVALID_AUTH_OPTION("compat_realm", gettext_noop("sspi"));
		if (strcmp(fc_val, "1") == 0)
			fc_hbaline->compat_realm = true;
		else
			fc_hbaline->compat_realm = false;
	}
	else if (strcmp(fc_name, "upn_username") == 0)
	{
		if (fc_hbaline->auth_method != uaSSPI)
			INVALID_AUTH_OPTION("upn_username", gettext_noop("sspi"));
		if (strcmp(fc_val, "1") == 0)
			fc_hbaline->upn_username = true;
		else
			fc_hbaline->upn_username = false;
	}
	else if (strcmp(fc_name, "radiusservers") == 0)
	{
		struct addrinfo *fc_gai_result;
		struct addrinfo fc_hints;
		int			fc_ret;
		List	   *fc_parsed_servers;
		ListCell   *fc_l;
		char	   *fc_dupval = pstrdup(fc_val);

		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius");

		if (!SplitGUCList(fc_dupval, ',', &fc_parsed_servers))
		{
			/* 列表中的语法错误 */
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("could not parse RADIUS server list \"%s\"",
							fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			return false;
		}

		/* 对于列表中的每个条目，进行翻译 */
		foreach(fc_l, fc_parsed_servers)
		{
			MemSet(&fc_hints, 0, sizeof(fc_hints));
			fc_hints.ai_socktype = SOCK_DGRAM;
			fc_hints.ai_family = AF_UNSPEC;

			fc_ret = pg_getaddrinfo_all((char *) lfirst(fc_l), NULL, &fc_hints, &fc_gai_result);
			if (fc_ret || !fc_gai_result)
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
								(char *) lfirst(fc_l), gai_strerror(fc_ret)),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));
				if (fc_gai_result)
					pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);

				list_free(fc_parsed_servers);
				return false;
			}
			pg_freeaddrinfo_all(fc_hints.ai_family, fc_gai_result);
		}

		/* 所有条目均正常，因此将其存储 */
		fc_hbaline->radiusservers = fc_parsed_servers;
		fc_hbaline->radiusservers_s = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "radiusports") == 0)
	{
		List	   *fc_parsed_ports;
		ListCell   *fc_l;
		char	   *fc_dupval = pstrdup(fc_val);

		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius");

		if (!SplitGUCList(fc_dupval, ',', &fc_parsed_ports))
		{
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("could not parse RADIUS port list \"%s\"",
							fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			*fc_err_msg = psprintf("invalid RADIUS port number: \"%s\"", fc_val);
			return false;
		}

		foreach(fc_l, fc_parsed_ports)
		{
			if (atoi(lfirst(fc_l)) == 0)
			{
				ereport(fc_elevel,
						(errcode(ERRCODE_CONFIG_FILE_ERROR),
						 errmsg("invalid RADIUS port number: \"%s\"", fc_val),
						 errcontext("line %d of configuration file \"%s\"",
									fc_line_num, HbaFileName)));

				return false;
			}
		}
		fc_hbaline->radiusports = fc_parsed_ports;
		fc_hbaline->radiusports_s = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "radiussecrets") == 0)
	{
		List	   *fc_parsed_secrets;
		char	   *fc_dupval = pstrdup(fc_val);

		REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius");

		if (!SplitGUCList(fc_dupval, ',', &fc_parsed_secrets))
		{
			/* 列表中的语法错误 */
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("could not parse RADIUS secret list \"%s\"",
							fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			return false;
		}

		fc_hbaline->radiussecrets = fc_parsed_secrets;
		fc_hbaline->radiussecrets_s = pstrdup(fc_val);
	}
	else if (strcmp(fc_name, "radiusidentifiers") == 0)
	{
		List	   *fc_parsed_identifiers;
		char	   *fc_dupval = pstrdup(fc_val);

		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius");

		if (!SplitGUCList(fc_dupval, ',', &fc_parsed_identifiers))
		{
			/* 列表中的语法错误 */
			ereport(fc_elevel,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("could not parse RADIUS identifiers list \"%s\"",
							fc_val),
					 errcontext("line %d of configuration file \"%s\"",
								fc_line_num, HbaFileName)));
			return false;
		}

		fc_hbaline->radiusidentifiers = fc_parsed_identifiers;
		fc_hbaline->radiusidentifiers_s = pstrdup(fc_val);
	}
	else
	{
		ereport(fc_elevel,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("unrecognized authentication option name: \"%s\"",
						fc_name),
				 errcontext("line %d of configuration file \"%s\"",
							fc_line_num, HbaFileName)));
		*fc_err_msg = psprintf("unrecognized authentication option name: \"%s\"",
							fc_name);
		return false;
	}
	return true;
}

/*
 * 扫描预解析的 hba 文件，寻找与端口连接请求的匹配项。
 */
static void fc_check_hba(hbaPort *fc_port)
{
	Oid			fc_roleid;
	ListCell   *fc_line;
	HbaLine    *fc_hba;

	/* 获取目标角色的 OID。注意，我们不会因为角色错误而报错。 */
	fc_roleid = get_role_oid(fc_port->user_name, true);

	foreach(fc_line, parsed_hba_lines)
	{
		fc_hba = (HbaLine *) lfirst(fc_line);

		/* 检查连接类型 */
		if (fc_hba->conntype == ctLocal)
		{
			if (fc_port->raddr.addr.ss_family != AF_UNIX)
				continue;
		}
		else
		{
			if (fc_port->raddr.addr.ss_family == AF_UNIX)
				continue;

			/* 检查 SSL 状态 */
			if (fc_port->ssl_in_use)
			{
				/* 连接为 SSL，匹配 "host" 和 "hostssl" */
				if (fc_hba->conntype == ctHostNoSSL)
					continue;
			}
			else
			{
				/* 连接不是 SSL，匹配 "host" 和 "hostnossl" */
				if (fc_hba->conntype == ctHostSSL)
					continue;
			}

			/* 检查 GSSAPI 状态 */
#ifdef ENABLE_GSS
			if (fc_port->gss && fc_port->gss->enc &&
				fc_hba->conntype == ctHostNoGSS)
				continue;
			else if (!(fc_port->gss && fc_port->gss->enc) &&
					 fc_hba->conntype == ctHostGSS)
				continue;
#else
			if (fc_hba->conntype == ctHostGSS)
				continue;
#endif

			/* 检查 IP 地址 */
			switch (fc_hba->ip_cmp_method)
			{
				case ipCmpMask:
					if (fc_hba->hostname)
					{
						if (!fc_check_hostname(fc_port,
											fc_hba->hostname))
							continue;
					}
					else
					{
						if (!fc_check_ip(&fc_port->raddr,
									  (struct sockaddr *) &fc_hba->addr,
									  (struct sockaddr *) &fc_hba->mask))
							continue;
					}
					break;
				case ipCmpAll:
					break;
				case ipCmpSameHost:
				case ipCmpSameNet:
					if (!fc_check_same_host_or_net(&fc_port->raddr,
												fc_hba->ip_cmp_method))
						continue;
					break;
				default:
					/* 不应该到这里，但如果是，则认为不匹配 */
					continue;
			}
		}						/* != ctLocal */

		/* 检查数据库和角色 */
		if (!fc_check_db(fc_port->database_name, fc_port->user_name, fc_roleid,
					  fc_hba->databases))
			continue;

		if (!fc_check_role(fc_port->user_name, fc_roleid, fc_hba->roles))
			continue;

		/* 找到一个匹配的记录! */
		fc_port->hba = fc_hba;
		return;
	}

	/* 如果没有找到匹配项，则隐式拒绝。 */
	fc_hba = palloc0(sizeof(HbaLine));
	fc_hba->auth_method = uaImplicitReject;
	fc_port->hba = fc_hba;
}

/*
 * 读取配置文件并为内容创建一个 HbaLine 记录列表。
 *
 * 配置被读取到一个临时列表中，如果发生任何解析错误，
 * 则保留旧列表并返回 false。 只有当整个文件都成功解析时，列表才会被替换，
 * 函数返回 true。
 *
 * 在 false 结果的情况下，调用者负责报告 FATAL 错误，以防
 * 这是初始启动。如果在重新加载时发生，我们只需继续使用
 * 旧数据运行。
 */
bool load_hba(void)
{
	FILE	   *fc_file;
	List	   *fc_hba_lines = NIL;
	ListCell   *fc_line;
	List	   *fc_new_parsed_lines = NIL;
	bool		fc_ok = true;
	MemoryContext fc_linecxt;
	MemoryContext fc_oldcxt;
	MemoryContext fc_hbacxt;

	fc_file = AllocateFile(HbaFileName, "r");
	if (fc_file == NULL)
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not open configuration file \"%s\": %m",
						HbaFileName)));
		return false;
	}

	fc_linecxt = tokenize_auth_file(HbaFileName, fc_file, &fc_hba_lines, LOG);
	FreeFile(fc_file);

	/* 现在解析所有行 */
	Assert(PostmasterContext);
	fc_hbacxt = AllocSetContextCreate(PostmasterContext,
								   "hba parser context",
								   ALLOCSET_SMALL_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_hbacxt);
	foreach(fc_line, fc_hba_lines)
	{
		TokenizedAuthLine *fc_tok_line = (TokenizedAuthLine *) lfirst(fc_line);
		HbaLine    *fc_newline;

		/* 不解析已经有错误的行 */
		if (fc_tok_line->err_msg != NULL)
		{
			fc_ok = false;
			continue;
		}

		if ((fc_newline = parse_hba_line(fc_tok_line, LOG)) == NULL)
		{
			/* 解析错误；请记住有问题 */
			fc_ok = false;

			/*
			 * 继续解析文件的其余部分，以便我们可以报告
			 * 不止第一行的错误。错误已经被记录，无需
			 * 更多的喧哗。
			 */
			continue;
		}

		fc_new_parsed_lines = lappend(fc_new_parsed_lines, fc_newline);
	}

	/*
	 * 有效的 HBA 文件必须至少有一个条目；否则无法
	 * 连接到 postmaster。 但只有在我们没有
	 * 解析错误的情况下才会抱怨这个问题。
	 */
	if (fc_ok && fc_new_parsed_lines == NIL)
	{
		ereport(LOG,
				(errcode(ERRCODE_CONFIG_FILE_ERROR),
				 errmsg("configuration file \"%s\" contains no entries",
						HbaFileName)));
		fc_ok = false;
	}

	/* 释放标记器内存 */
	MemoryContextDelete(fc_linecxt);
	MemoryContextSwitchTo(fc_oldcxt);

	if (!fc_ok)
	{
		/* 文件包含一个或多个错误，因此退出 */
		MemoryContextDelete(fc_hbacxt);
		return false;
	}

	/* 成功加载新文件，替换我们使用的文件 */
	if (parsed_hba_context != NULL)
		MemoryContextDelete(parsed_hba_context);
	parsed_hba_context = fc_hbacxt;
	parsed_hba_lines = fc_new_parsed_lines;

	return true;
}


/*
 * 从 ident 配置文件中解析一个标记化的行，并将结果存储在
 * IdentLine 结构中。
 *
 * 如果解析失败，记录一个级别为 elevel 的错误消息，
 * 将错误字符串存储在 tok_line->err_msg 中并返回 NULL。
 *
 * 如果 ident_user 是一个正则表达式（即以斜杠开头），
 * 它将被编译并存储在 IdentLine 结构中。
 *
 * 注意：当发生错误时，此函数会泄漏内存。调用者应该
 * 设置一个内存上下文，如果此函数返回 NULL，将重置该上下文。
 */
IdentLine *
parse_ident_line(TokenizedAuthLine *fc_tok_line, int fc_elevel)
{
	int			fc_line_num = fc_tok_line->line_num;
	char	  **fc_err_msg = &fc_tok_line->err_msg;
	ListCell   *fc_field;
	List	   *fc_tokens;
	AuthToken  *fc_token;
	IdentLine  *fc_parsedline;

	Assert(fc_tok_line->fields != NIL);
	fc_field = list_head(fc_tok_line->fields);

	fc_parsedline = palloc0(sizeof(IdentLine));
	fc_parsedline->linenumber = fc_line_num;

	/* 获取映射标记（必须存在） */
	fc_tokens = lfirst(fc_field);
	IDENT_MULTI_VALUE(fc_tokens);
	fc_token = linitial(fc_tokens);
	fc_parsedline->usermap = pstrdup(fc_token->string);

	/* 获取 ident 用户标记 */
	fc_field = lnext(fc_tok_line->fields, fc_field);
	IDENT_FIELD_ABSENT(fc_field);
	fc_tokens = lfirst(fc_field);
	IDENT_MULTI_VALUE(fc_tokens);
	fc_token = linitial(fc_tokens);
	fc_parsedline->ident_user = pstrdup(fc_token->string);

	/* 获取 PG 角色名称标记 */
	fc_field = lnext(fc_tok_line->fields, fc_field);
	IDENT_FIELD_ABSENT(fc_field);
	fc_tokens = lfirst(fc_field);
	IDENT_MULTI_VALUE(fc_tokens);
	fc_token = linitial(fc_tokens);
	fc_parsedline->pg_role = pstrdup(fc_token->string);

	if (fc_parsedline->ident_user[0] == '/')
	{
		/*
		 * 当系统用户名以斜杠开头时，将其视为正则表达式。
		 * 预编译它。
		 */
		int			fc_r;
		pg_wchar   *fc_wstr;
		int			fc_wlen;

		fc_wstr = palloc((strlen(fc_parsedline->ident_user + 1) + 1) * sizeof(pg_wchar));
		fc_wlen = pg_mb2wchar_with_len(fc_parsedline->ident_user + 1,
									fc_wstr, strlen(fc_parsedline->ident_user + 1));

		fc_r = pg_regcomp(&fc_parsedline->re, fc_wstr, fc_wlen, REG_ADVANCED, C_COLLATION_OID);
		if (fc_r)
		{
			char		fc_errstr[100];

			pg_regerror(fc_r, &fc_parsedline->re, fc_errstr, sizeof(fc_errstr));
			ereport(fc_elevel,
					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
					 errmsg("invalid regular expression \"%s\": %s",
							fc_parsedline->ident_user + 1, fc_errstr)));

			*fc_err_msg = psprintf("invalid regular expression \"%s\": %s",
								fc_parsedline->ident_user + 1, fc_errstr);

			pfree(fc_wstr);
			return NULL;
		}
		pfree(fc_wstr);
	}

	return fc_parsedline;
}

/*
 * 处理从解析的 ident 配置行中的一行。
 *
 * 将输入的解析 ident 行与所需的映射、pg_role 和 ident_user 进行比较。
 * *found_p 和 *error_p 根据我们的结果进行设置。
 */
static void fc_check_ident_usermap(IdentLine *fc_identLine, const char *fc_usermap_name,
					const char *fc_pg_role, const char *fc_ident_user,
					bool fc_case_insensitive, bool *fc_found_p, bool *fc_error_p)
{
	*fc_found_p = false;
	*fc_error_p = false;

	if (strcmp(fc_identLine->usermap, fc_usermap_name) != 0)
		/* 行不匹配我们要找的映射名称，因此直接退出 */
		return;

	/* 匹配? */
	if (fc_identLine->ident_user[0] == '/')
	{
		/*
		 * 当系统用户名以斜杠开头时，将其视为正则表达式。
		 * 在这种情况下，我们将系统用户名作为正则表达式处理，
		 * 它返回一个完全匹配的结果。如果存在，这将在数据库
		 * 用户名字符串中替换为 \1。
		 */
		int			fc_r;
		regmatch_t	fc_matches[2];
		pg_wchar   *fc_wstr;
		int			fc_wlen;
		char	   *fc_ofs;
		char	   *fc_regexp_pgrole;

		fc_wstr = palloc((strlen(fc_ident_user) + 1) * sizeof(pg_wchar));
		fc_wlen = pg_mb2wchar_with_len(fc_ident_user, fc_wstr, strlen(fc_ident_user));

		fc_r = pg_regexec(&fc_identLine->re, fc_wstr, fc_wlen, 0, NULL, 2, fc_matches, 0);
		if (fc_r)
		{
			char		fc_errstr[100];

			if (fc_r != REG_NOMATCH)
			{
				/* REG_NOMATCH 不是错误，其他所有的是 */
				pg_regerror(fc_r, &fc_identLine->re, fc_errstr, sizeof(fc_errstr));
				ereport(LOG,
						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
						 errmsg("regular expression match for \"%s\" failed: %s",
								fc_identLine->ident_user + 1, fc_errstr)));
				*fc_error_p = true;
			}

			pfree(fc_wstr);
			return;
		}
		pfree(fc_wstr);

		if ((fc_ofs = strstr(fc_identLine->pg_role, "\\1")) != NULL)
		{
			int			fc_offset;

			/* 请求的第一个参数的替换 */
			if (fc_matches[1].rm_so < 0)
			{
				ereport(LOG,
						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
						 errmsg("regular expression \"%s\" has no subexpressions as requested by backreference in \"%s\"",
								fc_identLine->ident_user + 1, fc_identLine->pg_role)));
				*fc_error_p = true;
				return;
			}

			/*
			 * 长度：原始长度减去 \1 的长度加上匹配的长度
			 * 加上空终止符
			 */
			fc_regexp_pgrole = palloc0(strlen(fc_identLine->pg_role) - 2 + (fc_matches[1].rm_eo - fc_matches[1].rm_so) + 1);
			fc_offset = fc_ofs - fc_identLine->pg_role;
			memcpy(fc_regexp_pgrole, fc_identLine->pg_role, fc_offset);
			memcpy(fc_regexp_pgrole + fc_offset,
				   fc_ident_user + fc_matches[1].rm_so,
				   fc_matches[1].rm_eo - fc_matches[1].rm_so);
			strcat(fc_regexp_pgrole, fc_ofs + 2);
		}
		else
		{
			/* 没有替换，因此复制匹配内容 */
			fc_regexp_pgrole = pstrdup(fc_identLine->pg_role);
		}

		/*
		 * 现在检查用户名是否与用户尝试连接的用户名实际匹配
		 */
		if (fc_case_insensitive)
		{
			if (pg_strcasecmp(fc_regexp_pgrole, fc_pg_role) == 0)
				*fc_found_p = true;
		}
		else
		{
			if (strcmp(fc_regexp_pgrole, fc_pg_role) == 0)
				*fc_found_p = true;
		}
		pfree(fc_regexp_pgrole);

		return;
	}
	else
	{
		/* 不是正则表达式，因此进行完全匹配 */
		if (fc_case_insensitive)
		{
			if (pg_strcasecmp(fc_identLine->pg_role, fc_pg_role) == 0 &&
				pg_strcasecmp(fc_identLine->ident_user, fc_ident_user) == 0)
				*fc_found_p = true;
		}
		else
		{
			if (strcmp(fc_identLine->pg_role, fc_pg_role) == 0 &&
				strcmp(fc_identLine->ident_user, fc_ident_user) == 0)
				*fc_found_p = true;
		}
	}
}


/*
 *	逐行扫描（预解析的）ident用户映射文件，寻找匹配项
 *
 *	检查ident用户名为"auth_user"的用户是否被允许作为
 *	Postgres用户"pg_role"根据用户映射"usermap_name"操作。
 *
 *	特例：用户映射为空，相当于之前称为
 *	"sameuser"或"samerole"，意味着不需要查看用户映射文件。
 *	这是一个隐含的映射，其中"pg_role"必须与
 *	"auth_user"完全相同才能获授权。
 *
 *	只有在授权的情况下，返回STATUS_OK，否则返回STATUS_ERROR。
 */
int check_usermap(const char *fc_usermap_name,
			  const char *fc_pg_role,
			  const char *fc_auth_user,
			  bool fc_case_insensitive)
{
	bool		fc_found_entry = false,
				fc_error = false;

	if (fc_usermap_name == NULL || fc_usermap_name[0] == '\0')
	{
		if (fc_case_insensitive)
		{
			if (pg_strcasecmp(fc_pg_role, fc_auth_user) == 0)
				return STATUS_OK;
		}
		else
		{
			if (strcmp(fc_pg_role, fc_auth_user) == 0)
				return STATUS_OK;
		}
		ereport(LOG,
				(errmsg("provided user name (%s) and authenticated user name (%s) do not match",
						fc_pg_role, fc_auth_user)));
		return STATUS_ERROR;
	}
	else
	{
		ListCell   *fc_line_cell;

		foreach(fc_line_cell, parsed_ident_lines)
		{
			fc_check_ident_usermap(lfirst(fc_line_cell), fc_usermap_name,
								fc_pg_role, fc_auth_user, fc_case_insensitive,
								&fc_found_entry, &fc_error);
			if (fc_found_entry || fc_error)
				break;
		}
	}
	if (!fc_found_entry && !fc_error)
	{
		ereport(LOG,
				(errmsg("no match in usermap \"%s\" for user \"%s\" authenticated as \"%s\"",
						fc_usermap_name, fc_pg_role, fc_auth_user)));
	}
	return fc_found_entry ? STATUS_OK : STATUS_ERROR;
}


/*
 * 读取ident配置文件并为内容创建IdentLine记录的列表。
 *
 * 这与load_hba()的工作方式相同，但用于用户配置文件。
 */
bool load_ident(void)
{
	FILE	   *fc_file;
	List	   *fc_ident_lines = NIL;
	ListCell   *fc_line_cell,
			   *fc_parsed_line_cell;
	List	   *fc_new_parsed_lines = NIL;
	bool		fc_ok = true;
	MemoryContext fc_linecxt;
	MemoryContext fc_oldcxt;
	MemoryContext fc_ident_context;
	IdentLine  *fc_newline;

	fc_file = AllocateFile(IdentFileName, "r");
	if (fc_file == NULL)
	{
		/* 并不致命 ... 我们只是不会执行任何特殊的ident映射 */
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not open usermap file \"%s\": %m",
						IdentFileName)));
		return false;
	}

	fc_linecxt = tokenize_auth_file(IdentFileName, fc_file, &fc_ident_lines, LOG);
	FreeFile(fc_file);

	/* 现在解析所有行 */
	Assert(PostmasterContext);
	fc_ident_context = AllocSetContextCreate(PostmasterContext,
										  "ident parser context",
										  ALLOCSET_SMALL_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_ident_context);
	foreach(fc_line_cell, fc_ident_lines)
	{
		TokenizedAuthLine *fc_tok_line = (TokenizedAuthLine *) lfirst(fc_line_cell);

		/* 不解析已经有错误的行 */
		if (fc_tok_line->err_msg != NULL)
		{
			fc_ok = false;
			continue;
		}

		if ((fc_newline = parse_ident_line(fc_tok_line, LOG)) == NULL)
		{
			/* 解析错误；请记住有问题 */
			fc_ok = false;

			/*
			 * 继续解析文件的其余部分，以便我们可以报告
			 * 不止第一行的错误。错误已经被记录，无需
			 * 更多的喧哗。
			 */
			continue;
		}

		fc_new_parsed_lines = lappend(fc_new_parsed_lines, fc_newline);
	}

	/* 释放标记器内存 */
	MemoryContextDelete(fc_linecxt);
	MemoryContextSwitchTo(fc_oldcxt);

	if (!fc_ok)
	{
		/*
		 * 文件包含一个或多个错误，因此退出，首先小心
		 * 清理我们分配的所有内容。大部分内容将通过
		 * MemoryContextDelete消失，但我们必须显式清理正则表达式。
		 */
		foreach(fc_parsed_line_cell, fc_new_parsed_lines)
		{
			fc_newline = (IdentLine *) lfirst(fc_parsed_line_cell);
			if (fc_newline->ident_user[0] == '/')
				pg_regfree(&fc_newline->re);
		}
		MemoryContextDelete(fc_ident_context);
		return false;
	}

	/* 成功加载新文件，替换我们使用的文件 */
	if (parsed_ident_lines != NIL)
	{
		foreach(fc_parsed_line_cell, parsed_ident_lines)
		{
			fc_newline = (IdentLine *) lfirst(fc_parsed_line_cell);
			if (fc_newline->ident_user[0] == '/')
				pg_regfree(&fc_newline->re);
		}
	}
	if (parsed_ident_context != NULL)
		MemoryContextDelete(parsed_ident_context);

	parsed_ident_context = fc_ident_context;
	parsed_ident_lines = fc_new_parsed_lines;

	return true;
}



/*
 *	确定在从前端"raddr"访问数据库
 *	"database"时应该使用什么认证方法，用户"user"。返回该方法和
 *	一个可选的参数（存储在*port的字段中），以及STATUS_OK。
 *
 *	如果文件不包含任何与请求匹配的条目，我们返回
 *	method = uaImplicitReject。
 */
void hba_getauthmethod(hbaPort *fc_port)
{
	fc_check_hba(fc_port);
}


/*
 * 返回正在使用的认证方法的名称（"gss"、"md5"、"trust"等）。
 *
 * 返回值是静态分配的（请参见UserAuthName数组）并且
 * 不应被释放。
 */
const char * hba_authname(UserAuth fc_auth_method)
{
	/*
	 * 确保UserAuthName[]跟踪对UserAuth枚举的添加
	 */
	StaticAssertStmt(lengthof(UserAuthName) == USER_AUTH_LAST + 1,
					 "UserAuthName[] must match the UserAuth enum");

	return UserAuthName[fc_auth_method];
}
